﻿HmJS.$Import('core.fx');

/*
---
version: 1.0
description: Provide a simple way to create 3D carousel.
license: MIT-style
download: http://mootools.net/forge/p/zoomer
source: http://github.com/creaven/Zoomer

copyright: Patrick Allain
authors: Patrick Allain

requires:
- core/1.2.4:: [Class.Extras, Element.Event, Element.Style, Element.Dimension, Fx.Morph]

provides: [Moo3DCarousel]
...
*/

HmJS.register('image.effect.Moo3DCarousel', function ($ns) {
	var $ = document.id;

	var Moo3DCarousel = new Class({

		/**
		* @see http://mootools.net/docs/core/Class/Class.Extras#Options
		*/
		Implements: [Events, Options],

		/**
		* Carousel options
		* Les options du carrousel
		*
		* @var Object JSON
		*/
		options: {
			// Le positionnement de l'overlay
			containerPosition: 'relative',

			// La marge que l'on dans l'overlay crée
			margin: 10,

			// L'offset du centre de l'elipse
			centerOffset: { x: 0, y: 0 },

			// La longueur du demi-grand axe
			xRadius: 300,
			// La longueur du demi-petit axe
			yRadius: 50,

			// La durée de d'une rotation
			rotateDuration: 500,

			// Les options pour les transitions
			fx: { link: 'cancel' },

			// Ratio minimale que peut prendre une image
			ratioMin: 0.4,

			// Angle offset
			offsetAngle: 0,

			// On définit le départ pour la propriété z-index
			zIndex: 100,

			// Exposant pour la fonction modulant la position
			powerExponent: 0.85,

			// Ajoute l'événent mousewheel
			mouseWheel: true,

			// Si on veut faire un autorun
			autoRun: true,
			interval: 3000,

			// Stop le carrousel lorsque l'on le survole
			stopOver: true,

			// Sens trigo ou non
			trigo: true
		},

		/**
		* The container element
		* L'élément container
		*
		* @var element
		*/
		_container: null,

		/**
		* The carousel elements
		* Les éléments constitutifs du Moo3DCarousel
		*
		* @var	element
		*/
		_elements: [],

		/**
		* Max size of a images
		* La taille maximale que peut prendre une image
		*
		* @var	Object JSON
		*/
		_sizeMax: { x: 0, y: 0 },

		/**
		* Index of the current image focused
		* L'index de l'image mise en avant
		*
		* @var int
		*/
		_currentIndex: 0,

		/**
		* Index of the last image focused
		* L'index de l'image mise en avant précédente
		*
		* @var int
		*/
		_lastIndex: -1,

		/**
		* Elipse center
		* Le centre de l'ellipse
		*
		* @var	Object	JSON
		*/
		_center: { x: 0, y: 0 },

		/**
		* The events methods
		* Les méthodes déclencher lors d'événements
		*
		* @var	Object
		*/
		_evFunct: {},

		/**
		* Timer
		*
		* @var	timer
		*/
		_periodical: null,

		/**
		* Autorun
		*
		* @var	bool
		*/
		_autorun: false,

		/**
		* Class constructor
		* Le constructeur de la classe.
		*
		* @param		element		parent
		* @param 		Object JSON		options
		*/
		initialize: function (parent, options) {
			parent = $(parent);
			this.setOptions(options);

			// On crée un nouvel élément et on définit sa position puis on l'injecte dans l'élément parent
			this._container = new Element('div', { 'class': 'Moo3DCarouselOverlay' }).setStyles({
				'position': this.options.containerPosition,
				'z-index': this.options.zIndex
			}).inject(parent);

			// On crée notre objet contenant toutes les méthodes déclenchée lors d'un événement
			this._evFunct = {
				// Vous pourrez changer le signe en fonction de comment cela vous parait le plus naturel
				wheel: function (ev) { ev.stop(); this[(ev.wheel == 1) ? 'goNext' : 'goPrevious'](); } .bind(this),
				winResize: function (ev) { this.init(); } .bind(this),
				mouseEnter: function (ev) { clearInterval(this._periodical); this.fireEvent('stop'); this._autorun = false; } .bind(this),
				mouseLeave: function (ev) {
					clearInterval(this._periodical);
					this._periodical = this.goNext.periodical(this.options.interval, this);
					this.fireEvent('start');
					this._autorun = true;
				} .bind(this)
			};

			// On ajoute l'événement sur l'overlay
			if (this.options.mouseWheel) {
				this._container.addEvent('mousewheel', this._evFunct.wheel);
			}

			// On réinitialise lors du redimensionnement de la fenêtre
			window.addEvent('resize', this._evFunct.winResize);

			if (this.options.autoRun) {
				this._evFunct.mouseLeave();
				if (this.options.stopOver) {
					// On ajoute les événements dans le cas d'une rotation continue
					this._container.addEvents({
						'mouseenter': this._evFunct.mouseEnter,
						'mouseleave': this._evFunct.mouseLeave
					});
				}
			}
		},

		/**
		* Add a element in the carousel
		* Ajoute un élément au carrousel
		*
		* @param		element		elem
		* @param		Object JSON		size
		* @return 		Moo3DCarousel
		*/
		addElement: function (elem, size) {
			// On récupère l'image et on l'ajoute au tableau
			elem = $(elem);

			// On injecte l'élément dans l'overlay.
			elem.inject(this._container);

			// On ajoute l'élément à la liste des éléments du tableaux
			this._elements.push(elem);

			// Dans une premier temps, on récupère l'élément image
			var imgEl = (elem.get('tag') == 'img') ? elem : elem.getElement('img');

			// On définit la position de l'image en absolu
			imgEl.setStyle('position', 'absolute');

			// On définit l'objet size si celui-ci n'a pas été passez en argument.
			size = size || {};

			// Puis on récupère la taille de l'image.
			size.x = size.x || (imgEl.getProperty('width') || '').toInt() || imgEl.getSize().x;
			size.y = size.y || (imgEl.getProperty('height') || '').toInt() || imgEl.getSize().y;

			// On stocke cette taille sur l'élément.
			elem.store("Moo3DCarousel:size", size);

			// On recalcule la taille maximale
			this._sizeMax = {
				x: Math.max(size.x, this._sizeMax.x),
				y: Math.max(size.y, this._sizeMax.y)
			};

			// On ajoute l'objet permettant de faire des transitions sur l'image
			elem.store("Moo3DCarousel:fx", new Fx.Morph(imgEl, Object.append({ duration: this.options.rotateDuration }), this.options.fx));

			// On réinitialise le carrousel afin de prendre en compte le nouvel élément
			this.init();

			return this;
		},

		/**
		* Retrieve the carousel size
		* Retourne la taille du carrousel
		*
		* @return		Object JSON
		*/
		getSize: function () {
			var _options = this.options;
			return {
				x: this._sizeMax.x + 2 * (_options.margin + _options.xRadius),
				y: this._sizeMax.y + 2 * (_options.margin + _options.yRadius)
			};
		},

		/**
		* Position images without transition
		* Repositionne les images sans effectuer de transition
		*
		* @return		Moo3DCarousel
		*/
		init: function (index) {

			this._currentIndex = index || this._currentIndex;

			// On re-calcule le centre de l'elipse
			this._center = {
				x: (this._container.getSize().x / 2) + this.options.centerOffset.x,
				y: this.options.centerOffset.y + this.options.yRadius
			};

			// On rédéfinit la taille de l'overlay afin que les images ne sorte pas du flux
			this._container.setStyle('height', this.getSize().y);

			var els = this._elements;
			var l = els.length;

			// On replace tous les éléments
			for (var i = 0; i < l; i++) {
				var elem = els[i];

				// On récupère l'élément image
				var imgEl = (elem.get('tag') == 'img') ? elem : elem.getElement('img');

				// On calcule le décalage de l'image par rapport à l'élément mis en avant.
				var index = (i + l - this._currentIndex) % l;

				// On récupère l'angle de la position de l'image sur l'ellipse
				var teta = this._getTeta(index);

				// On récupère le style que l'on doit appliquer sur l'image
				var styles = this._getStyles(elem, teta);

				// On applique le style sur l'image
				imgEl.setStyles(styles);
			}

			// Déclenche l'événement init
			this.fireEvent('init');

			return this;
		},

		/**
		* Transform a index in an angle.
		* Retourne l'angle à partir d'une position d'index
		*
		* @param		int				index
		* @return		float
		*/
		_getTeta: function (index) {
			var elsLen = this._elements.length,
					pi = Math.PI;
			index = index % elsLen;

			var teta = (index / elsLen) * 2 * pi;

			teta = (teta + pi) % (2 * pi) - pi;
			teta = ((teta > 0) ? 1 : -1) * pi * Math.pow(Math.abs(teta) / pi, this.options.powerExponent);

			return teta;
		},

		/**
		* Fix the style to apply on a element from angle
		* Calcul le style à appliquer sur un élément du carrousel
		* en fonction de l'angle sous lequel il est vu
		*
		* @param		element		elem
		* @param		float			teta
		* @return		Object JSON
		*/
		_getStyles: function (elem, teta) {
			// On calcule la rotation du carrousel
			var size = elem.retrieve('Moo3DCarousel:size', elem.getSize());
			var _options = this.options;

			// On calcule le coefficient pour le modulant de la taille et de la propriété z-index
			var sCoeff = (1 - _options.ratioMin) / 2;
			sCoeff = 1 + sCoeff * (Math.cos(teta) - 1);

			// On calcule la taille de l'image modulé par la fonction présenté
			var out = {
				width: (size.x * sCoeff).toInt(),
				height: (size.y * sCoeff).toInt()
			};

			// On ajoute la propriété z-index
			Object.append(out, {
				'z-index': (_options.zIndex + sCoeff * 2 * this._elements.length).toInt()
			});

			// On ajoute la position en décalant horizontalement l'image de la moitié sa largeur
			Object.append(out, {
				left: (this._center.x + _options.xRadius * Math.sin(teta + _options.offsetAngle) - out.width / 2).toInt(),
				top: (this._center.y + _options.yRadius * Math.cos(teta + _options.offsetAngle)).toInt()
			});

			return out;
		},

		/**
		* Focus a element's carousel
		* Permet de mettre en avant une photo
		*
		* @var			element|id		element
		* @return 		Moo3DCarousel
		*/
		focus: function (elem) {
			elem = $(elem);
			var index = this._elements.indexOf(elem);
			if (index != -1) {
				this.goTo(index);
			}
			return this;
		},

		/**
		* Focus the previous element
		* Permet d'accéder à l'élément précédent
		*
		* @return		Moo3DCarousel
		*/
		goPrevious: function () {
			return this.goTo(this._currentIndex - (this.options.trigo ? -1 : 1));
		},

		/**
		* Focus the next element
		* Permet d'accéder à l'élément suivant
		*
		* @return		Moo3DCarousel
		*/
		goNext: function () {
			return this.goTo(this._currentIndex + (this.options.trigo ? -1 : 1));
		},

		/**
		* Focus a element from its index
		* Permet d'accéder à un élément en fonction de son index
		*
		* @param		int				index
		* @return		Moo3DCarousel
		*/
		goTo: function (index) {

			this.init();

			var els = this._elements;
			var l = els.length;

			// On garde sous la main l'index de l'image précédente
			this._lastIndex = this._currentIndex;
			// Nous refinissons notre l'index de l'image mise en avant
			this._currentIndex = (index + l) % l;

			// On déplace tous les éléments avec des transitions
			for (var i = 0; i < l; i++) {
				var elem = els[i];

				// On récupère l'élément de transition
				var fx = elem.retrieve("Moo3DCarousel:fx");

				// On calcule le décalage de l'image par rapport à l'élément mis en avant.
				var index = (i + l - this._currentIndex) % l;

				// On récupère l'angle de la position de l'image sur l'ellipse
				var teta = this._getTeta(index);

				// On récupère le style que l'on doit appliquer sur l'image
				var styles = this._getStyles(elem, teta);

				// On applique la transition
				fx.start(styles);
			}

			// Si on a l'autorun, remet le compteur à zéro
			if (this._autorun) {
				clearInterval(this._periodical);
				this._periodical = this.goNext.periodical(this.options.interval, this);
			}

			// Déclenche l'événement rotateStart
			this.fireEvent('rotateStart', this);

			// Déclenche l'événement rotateEnd
			// prevent some side effect
			this.fireEvent('rotateEnd', this, this.options.rotateDuration);

			return this;
		},

		/**
		* Return the number of element into the carousel
		* Retourne le nombre d'élément dans le carousel
		*
		* @return	int
		*/
		count: function () {
			return this._elements.length;
		},

		/**
		*
		* @return		element
		*/
		toElement: function () {
			return this._container;
		},

		getLastFocused: function () {
			if (this._lastIndex < 0) {
				return null;
			}
			var els = this._elements,
					l = els.length;
			return els[((this._lastIndex + l) % l)];
		},

		getFocused: function () {
			var els = this._elements,
					l = els.length;
			return this.els[((this._currentIndex + l) % l)];
		}

	});

	Element.implement({

		moo3DCarousel: function (options) {
			var children = this.getChildren(),
					moo3DCarousel = new Moo3DCarousel(this, options);
			children.each(function (el) {
				moo3DCarousel.addElement(el);
			});
			return this;
		}

	});

	return Moo3DCarousel;
});